// wifi_provisioning_server.c
/**
 * 杰理AC79xx WiFi AP配网服务器模块（模块化/可移植/可启停）
 *
 * 依赖（请在wifi task中启动DHCP与DNS）:
 *   dhcps_offer_dns();
 *   u32 dns_server = 0xC0A80101; // 192.168.1.1
 *   dns_setserver(0, &dns_server);
 *   dhcps_init(0);
 */
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include "sock_api/sock_api.h"
#include "os/os_api.h"
#include "app_config.h"
#include "system/includes.h"
#include "wifi/wifi_connect.h"
#include "lwip.h"
#include "lwip/sockets.h"
#include "lwip/netdb.h"
#include "dhcp_srv/dhcp_srv.h"

#include "wifi_provisioning_server.h"

// =============== 配置与上下文 ===============
#define DEFAULT_HTTP_PORT 80
#define DEFAULT_CAPTIVE_IP 0xC0A80101 // 192.168.1.1

typedef struct
{
    wifi_prov_server_state_t state;
    wifi_prov_server_config_t cfg;
    wifi_prov_server_callbacks_t cbs;

    int server_pid;
    int accept_pid;
    void *server_socket;

    OS_MUTEX mtx;
    u8 initialized;
    u8 autostart_enabled;
} prov_ctx_t;

static prov_ctx_t g_ctx;

// =============== 前端页面（只调整布局，不改业务） ===============
static const char index_body[] =
    "<!doctype html><html><head><meta charset='utf-8'><title>WiFi 配网</title>"
    "<meta name='viewport' content='width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no'/>"
    "<style>"
    ":root{--bg:#f5f7fb;--card:#fff;--txt:#1f2328;--muted:#6b7380;--line:#e6e8eb;--line2:#f0f1f3;--pri:#0b6bff;--pri-weak:rgba(11,107,255,.12);--shadow:0 6px 24px rgba(31,35,40,.06);--ok:#127c3e;--warn:#9a6c00;--err:#c21d2e}"
    "@media(prefers-color-scheme:dark){:root{--bg:#0c1116;--card:#0f141a;--txt:#e6edf3;--muted:#9aa4b2;--line:#202733;--line2:#1b2230;--pri:#4c8dff;--pri-weak:rgba(76,141,255,.16);--shadow:0 8px 28px rgba(0,0,0,.45)}}"
    "html,body{height:100%}"
    "body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;margin:0;background:var(--bg);color:var(--txt);-webkit-tap-highlight-color:transparent}"
    ".wrap{min-height:100%;display:flex;align-items:center;justify-content:center;padding:clamp(12px,3vw,24px)}"
    ".card{width:min(760px,92vw);background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);overflow:hidden}"
    ".hd{padding:16px 20px;border-bottom:1px solid var(--line2);background:linear-gradient(180deg,#fafbfc00,rgba(255,255,255,.06))}"
    ".title{margin:0;font-size:clamp(17px,2.2vw,20px);font-weight:800;color:var(--txt);display:flex;align-items:center;gap:10px;letter-spacing:.2px}"
    ".title:before{content:\"\";display:inline-block;width:10px;height:10px;border-radius:50%;background:var(--pri);box-shadow:0 0 0 3px var(--pri-weak)}"
    ".bd{padding:clamp(14px,3vw,22px)}"
    ".row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}"
    "label{display:block;margin:.6rem 0 .35rem;color:var(--muted);font-size:13px}"
    "input,button,select{font-size:16px}"
    "input{width:100%;padding:.78rem .9rem;border:1px solid var(--line);background:transparent;color:var(--txt);border-radius:10px;outline:none;transition:border .15s ease,box-shadow .15s ease}"
    "input::placeholder{color:#9aa4b2}"
    "input:focus{border-color:var(--pri);box-shadow:0 0 0 4px var(--pri-weak)}"
    ".btn{display:inline-flex;align-items:center;gap:.5em;background:var(--pri);color:#fff;border:none;border-radius:999px;padding:.78rem 1.15rem;cursor:pointer;transition:transform .08s ease,box-shadow .15s ease;letter-spacing:.2px}"
    ".btn:hover{box-shadow:0 10px 24px rgba(11,107,255,.28)}"
    ".btn:active{transform:translateY(1px)}"
    ".btn.sec{background:#636c78}"
    ".muted{color:var(--muted)}"
    ".list{margin-top:12px;max-height:min(320px,42vh);overflow:auto;border:1px solid var(--line);border-radius:10px;background:transparent}"
    ".item{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line2);cursor:pointer}"
    ".item:hover{background:rgba(127,127,127,.06)}"
    ".item:last-child{border-bottom:none}"
    ".ssid{font-weight:700;letter-spacing:.15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70%}"
    ".rssi{font-size:12px;color:var(--muted)}"
    ".msg{margin-top:12px;font-size:14px;white-space:pre-wrap}"
    ".ok{color:var(--ok)}.err{color:var(--err)}.warn{color:var(--warn)}"
    ".toast{position:fixed;left:50%;top:14px;transform:translateX(-50%);background:#111927;color:#fff;padding:.7rem 1rem;border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.35);z-index:9999;opacity:0;pointer-events:none;transition:opacity .2s}"
    ".toast.show{opacity:1}"
    ".hr{height:1px;background:linear-gradient(90deg,transparent,var(--line2),transparent);margin:16px 0}"
    ".two-col{display:grid;grid-template-columns:1fr 1fr;gap:clamp(10px,2.2vw,18px)}"
    "@media(max-width:720px){.two-col{grid-template-columns:1fr}.btn{width:100%;justify-content:center}.row.wrap-to-col{flex-direction:column;align-items:stretch}}"
    ".hint{font-size:12px;color:var(--muted)}"
    ".connecting{padding:26px 14px;text-align:center}"
    ".spinner{width:48px;height:48px;border-radius:50%;border:4px solid var(--line);border-top-color:var(--pri);margin:10px auto 14px;animation:sp 1s linear infinite}"
    "@keyframes sp{to{transform:rotate(360deg)}}"
    ".success-icon{width:48px;height:48px;border-radius:50%;background:var(--ok);color:#fff;display:flex;align-items:center;justify-content:center;font-size:24px;margin:10px auto 14px;animation:successPulse 1s ease-out}"
    "@keyframes successPulse{0%{transform:scale(0);opacity:0}50%{transform:scale(1.1)}100%{transform:scale(1);opacity:1}}"
    ".steps{margin:10px auto 0;text-align:left;max-width:560px;color:#5a6472;line-height:1.7}"
    ".steps li{margin:.25rem 0;opacity:0;transform:translateY(10px);animation:fadeInUp .6s ease forwards}"
    ".steps li:nth-child(1){animation-delay:.2s}"
    ".steps li:nth-child(2){animation-delay:.4s}"
    ".steps li:nth-child(3){animation-delay:.6s}"
    ".steps li:nth-child(4){animation-delay:.8s}"
    ".steps li:nth-child(5){animation-delay:1s}"
    "@keyframes fadeInUp{to{opacity:1;transform:translateY(0)}}"
    ".badge{display:inline-flex;align-items:center;gap:6px;padding:.28rem .56rem;border-radius:999px;background:rgba(11,107,255,.12);color:var(--pri);font-weight:800;font-size:12px;letter-spacing:.3px}"
    ".big{font-size:15px}"
    ".countdown{display:inline-flex;align-items:center;gap:8px;margin:12px 0;padding:8px 16px;background:rgba(11,107,255,.08);border-radius:20px;font-weight:600;color:var(--pri)}"
    ".countdown-num{font-size:18px;font-weight:800;min-width:24px;text-align:center}"
    ".progress-bar{width:100%;height:4px;background:var(--line2);border-radius:2px;overflow:hidden;margin:16px 0}"
    ".progress-fill{height:100%;background:linear-gradient(90deg,var(--pri),#4c8dff);border-radius:2px;width:0%;transition:width .3s ease}"
    ".status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px;background:var(--muted)}"
    ".status-dot.active{background:var(--ok);animation:pulse 1.5s ease-in-out infinite}"
    "@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.1)}}"
    ".result-section{display:none;margin-top:20px;padding:20px;background:rgba(18,124,62,.08);border-radius:12px;border:1px solid rgba(18,124,62,.2)}"
    ".result-section.show{display:block;animation:fadeInUp .6s ease}"
    ".reset-info{margin-top:16px;padding:16px;background:rgba(155,108,0,.08);border-radius:8px;border:1px solid rgba(155,108,0,.2)}"
    ".reset-info h4{margin:0 0 8px;color:var(--warn);font-size:14px;font-weight:600}"
    ".reset-info p{margin:4px 0;font-size:13px;color:var(--muted);line-height:1.5}"
    ".i{font-style:normal;opacity:.9}"
    "</style>"
    "</head><body><div class='wrap'>"
    "<div class='card'>"
    "  <div class='hd'><h1 class='title'>📶 WiFi 配网</h1></div>"
    "  <div class='bd'>"
    "    <div id='page-form'>"
    "      <div class='row' style='justify-content:space-between'>"
    "        <div class='badge'>✨ 配网向导</div>"
    "        <span id='scanHint' class='hint'>请输入WiFi信息或使用扫描功能</span>"
    "      </div>"
    "      <div class='hr'></div>"
    "      <form onsubmit='ev(event)'>"
    "        <div class='two-col'>"
    "          <div>"
    "            <label>📶 SSID</label>"
    "            <input id='ssid' required maxlength='63' placeholder='请输入WiFi名称'/>"
    "          </div>"
    "          <div>"
    "            <label>🔒 密码</label>"
    "            <input id='pwd' type='password' maxlength='63' placeholder='请输入WiFi密码'/>"
    "          </div>"
    "        </div>"
    "        <div class='row' style='margin-top:12px;justify-content:space-between'>"
    "          <label class='muted' style='display:flex;align-items:center;gap:8px;margin:0'><input id='save' type='checkbox' checked /> 保存到设备</label>"
    "          <div class='row' style='gap:10px'>"
    "            <button class='btn' type='submit' id='connBtn'>🟢 连接</button>"
    "          </div>"
    "        </div>"
    "      </form>"
    "      <div id='msg' class='msg muted'></div>"
    "      <div class='hr'></div>"
    "      <div class='row wrap-to-col' style='gap:12px'>"
    "        <button class='btn sec' id='scanBtn'>🔍 扫描附近WiFi</button>"
    "      </div>"
    "      <div id='scanList' class='list' style='display:none;margin-top:10px'></div>"
    "    </div>"
    "    <div id='page-connecting' style='display:none'>"
    "      <div class='connecting'>"
    "        <div class='spinner' id='spinner'></div>"
    "        <div class='success-icon' id='successIcon' style='display:none'>✅</div>"
    "        <div class='big' style='font-weight:700' id='statusTitle'>⏳ 正在连接路由器…</div>"
    "        <div id='connSsid' class='muted' style='margin-top:6px'></div>"
    "        <div class='countdown' id='countdown'>"
    "          <span>⏰ 预计剩余时间：</span>"
    "          <span class='countdown-num' id='countdownNum'>15</span>"
    "          <span>秒</span>"
    "        </div>"
    "        <div class='progress-bar'>"
    "          <div class='progress-fill' id='progressFill'></div>"
    "        </div>"
    "        <div class='hr'></div>"
    "        <ul class='steps' id='stepsList'>"
    "          <li><span class='status-dot active'></span>✅ 设备已收到配网信息，正在切换至 STA 模式</li>"
    "          <li><span class='status-dot'></span>🔄 开始连接路由器，验证WiFi密码</li>"
    "          <li><span class='status-dot'></span>📡 获取IP地址，建立网络连接</li>"
    "          <li><span class='status-dot'></span>🌐 连接成功，设备即将切换网络</li>"
    "          <li><span class='status-dot'></span>📱 手机将断开设备热点，请重新连接WiFi</li>"
    "        </ul>"
    "        <div class='hr'></div>"
    "        <div class='hint'>💡 提示：连接过程中请保持设备与路由器距离适中，避免信号干扰</div>"
    "        <div class='result-section' id='resultSection'>"
    "          <div style='text-align:center;margin-bottom:16px'>"
    "            <div style='font-size:18px;font-weight:700;color:var(--ok);margin-bottom:8px'>🎉 配网完成！</div>"
    "            <div style='color:var(--muted);font-size:14px'>设备已成功连接到WiFi网络</div>"
    "          </div>"
    "          <div style='background:rgba(18,124,62,.1);padding:12px;border-radius:8px;margin-bottom:16px'>"
    "            <div style='font-weight:600;color:var(--ok);margin-bottom:4px'>📋 下一步操作：</div>"
    "            <div style='font-size:13px;color:var(--muted);line-height:1.6'>"
    "              1. 返回手机WiFi设置，重新连接到您的路由器<br/>"
    "              2. 检查设备指示灯是否显示联网状态<br/>"
    "              3. 如设备未联网，请重新进入配网模式"
    "            </div>"
    "          </div>"
    "          <div class='reset-info'>"
    "            <h4>🔧 重置方法</h4>"
    "            <p><strong>重新配网：</strong>长按设备按键 5 秒，设备将重新进入配网模式</p>"
    "            <p><strong>恢复出厂：</strong>长按设备按键 10 秒，清除所有配置并恢复出厂设置</p>"
    "            <p><strong>手动配网：</strong>断开设备电源 10 秒后重新上电，首次启动自动进入配网模式</p>"
    "          </div>"
    "        </div>"
    "      </div>"
    "    </div>"
    "  </div>"
    "</div>"
    "<div id='toast' class='toast' role='status' aria-live='polite'></div>"
    "</div>"
    "<script>"
    "const $=s=>document.querySelector(s);"
    "const scanBtn=$('#scanBtn'), scanList=$('#scanList'), scanHint=$('#scanHint'), msg=$('#msg');"
    "const pageForm=$('#page-form'), pageConn=$('#page-connecting'), connSsid=$('#connSsid');"
    "const countdownNum=$('#countdownNum'), progressFill=$('#progressFill'), stepsList=$('#stepsList');"
    "const spinner=$('#spinner'), successIcon=$('#successIcon'), statusTitle=$('#statusTitle');"
    "const countdown=$('#countdown'), resultSection=$('#resultSection');"
    "let countdownTimer = null, stepTimer = null;"
    "function note(t,cls=''){msg.className='msg '+(cls||''); msg.textContent=t}"
    "function tip(t,dur=3200){const el=$('#toast'); el.textContent=t; el.classList.add('show'); setTimeout(()=>el.classList.remove('show'),dur)}"
    "function fill(ssid){$('#ssid').value=ssid; note('已选择: '+ssid,'ok')}"
    "function rssiBar(r){return (r||0)+' dBm'}"
    "function esc(s){return String(s||'').replace(/[&<>\\\"']/g,c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;' }[c]))}"
    "function startCountdown() {"
    "  let time = 15;"
    "  const totalTime = 15;"
    "  countdownTimer = setInterval(() => {"
    "    time--;"
    "    countdownNum.textContent = time;"
    "    const progress = ((totalTime - time) / totalTime) * 100;"
    "    progressFill.style.width = progress + '%';"
    "    if (time <= 0) {"
    "      clearInterval(countdownTimer);"
    "      countdownNum.textContent = '0';"
    "      progressFill.style.width = '100%';"
    "      spinner.style.display = 'none';"
    "      successIcon.style.display = 'flex';"
    "      statusTitle.textContent = '🎉 配网完成！';"
    "      statusTitle.style.color = 'var(--ok)';"
    "      countdown.style.display = 'none';"
    "      resultSection.classList.add('show');"
    "      tip('配网完成！请检查设备联网状态', 5000);"
    "    }"
    "  }, 1000);"
    "}"
    "function startStepAnimation() {"
    "  const steps = stepsList.querySelectorAll('li');"
    "  let currentStep = 0;"
    "  stepTimer = setInterval(() => {"
    "    if (currentStep < steps.length) {"
    "      const dot = steps[currentStep].querySelector('.status-dot');"
    "      if (dot) dot.classList.add('active');"
    "      currentStep++;"
    "    } else {"
    "      clearInterval(stepTimer);"
    "    }"
    "  }, 3000);"
    "}"
    "function stopAllTimers() {"
    "  if (countdownTimer) clearInterval(countdownTimer);"
    "  if (stepTimer) clearInterval(stepTimer);"
    "}"
    "async function scan(){"
    " try{scanBtn.disabled=true; scanHint.textContent='扫描中…';"
    "  const r=await fetch('/scan',{cache:'no-store'}); const arr=await r.json();"
    "  if(Array.isArray(arr)&&arr.length){"
    "    scanList.style.display='block';"
    "    scanList.innerHTML=arr.map(o=>`<div class=\\\"item\\\" data-ssid=\\\"${esc(o.ssid)}\\\"><span class=\\\"ssid\\\">${esc(o.ssid)||'(隐藏)'}<\\/span><span class='rssi'>📶 ${rssiBar(o.rssi)}<\\/span><\\/div>`).join('');"
    "  }else{scanList.style.display='none'; note('未扫描到可用热点','warn')}"
    " }catch(e){note('扫描失败: '+e,'err')}finally{scanBtn.disabled=false; scanHint.textContent='可再次扫描，或手动输入'}"
    "}"
    "function showConnecting(name){"
    " pageForm.style.display='none';"
    " pageConn.style.display='block';"
    " connSsid.textContent='目标路由器：'+(name||'-');"
    " stopAllTimers();"
    " startCountdown();"
    " startStepAnimation();"
    "}"
    "async function ev(e){e.preventDefault();"
    " const s=$('#ssid').value.trim(), p=$('#pwd').value, save=$('#save').checked?1:0;"
    " if(!s){note('SSID 不能为空','err'); return}"
    " $('#connBtn').disabled=true; note('正在提交…');"
    " try{const u='/wifi_cfg?ssid='+encodeURIComponent(s)+'&pwd='+encodeURIComponent(p)+'&save='+save;"
    "  const r=await fetch(u,{cache:'no-store'}); const j=await r.json();"
    "  if(j.ok){"
    "    tip('已提交，设备即将切换网络');"
    "    showConnecting(s);"
    "  }else{"
    "    note('提交失败: '+(j.msg||''),'err');"
    "  }"
    " }catch(e){note('请求失败: '+e,'err')}finally{$('#connBtn').disabled=false}"
    "}"
    "scanBtn.onclick=scan;"
    "setTimeout(scan,800);"
    "scanList.addEventListener('click',e=>{const it=e.target.closest('.item'); if(it&&it.dataset.ssid!=null){ fill(it.dataset.ssid); }});"
    "</script>"
    "</body></html>";

// =============== 工具函数 ===============
static void url_decode(char *s)
{
    char *p = s, *o = s;
    while (*p)
    {
        if (*p == '+')
        {
            *o++ = ' ';
            p++;
        }
        else if (*p == '%' && p[1] && p[2])
        {
            int hi = (p[1] >= 'A') ? ((p[1] & ~0x20) - 'A' + 10) : (p[1] - '0');
            int lo = (p[2] >= 'A') ? ((p[2] & ~0x20) - 'A' + 10) : (p[2] - '0');
            if ((unsigned)hi < 16 && (unsigned)lo < 16)
            {
                *o++ = (char)((hi << 4) | lo);
                p += 3;
            }
            else
            {
                *o++ = *p++;
            }
        }
        else
        {
            *o++ = *p++;
        }
    }
    *o = '\0';
}

static void query_get(const char *query, const char *key, char *out, int out_len)
{
    out[0] = 0;
    if (!query || !key)
        return;
    const char *p = query;
    int klen = strlen(key);
    while (p && *p)
    {
        const char *amp = strchr(p, '&');
        int seglen = amp ? (int)(amp - p) : (int)strlen(p);
        if (seglen > klen + 1 && !memcmp(p, key, klen) && p[klen] == '=')
        {
            int vlen = seglen - (klen + 1);
            if (vlen >= out_len)
                vlen = out_len - 1;
            memcpy(out, p + klen + 1, vlen);
            out[vlen] = 0;
            url_decode(out);
            return;
        }
        p = amp ? (amp + 1) : NULL;
    }
}

static int json_escape(char *dst, int dst_len, const char *src)
{
    int n = 0;
    for (; src && *src && n < dst_len - 1; src++)
    {
        unsigned char c = (unsigned char)*src;
        if (c == '\"' || c == '\\')
        {
            if (n >= dst_len - 2)
                break;
            dst[n++] = '\\';
            dst[n++] = c;
        }
        else if (c < 0x20)
        {
            // skip
        }
        else
        {
            dst[n++] = c;
        }
    }
    dst[n] = 0;
    return n;
}

// =============== HTTP响应封装 ===============
static void send_http_text(int cfd, const char *ctype, const char *fmt, ...)
{
    char body[512];
    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(body, sizeof(body), fmt, ap);
    va_end(ap);
    if (n < 0)
        n = 0;
    if (n > (int)sizeof(body))
        n = sizeof(body);

    char hdr[256];
    int hn = snprintf(hdr, sizeof(hdr),
                      "HTTP/1.1 200 OK\r\nContent-Type: %s; charset=utf-8\r\nConnection: close\r\nContent-Length: %d\r\n\r\n",
                      ctype ? ctype : "text/plain", n);
    sock_send(cfd, hdr, hn, 0);
    sock_send(cfd, body, n, 0);
}

static void send_http_html(int cfd, const char *html, int len)
{
    if (len < 0)
        len = strlen(html);
    char hdr[256];
    int hn = snprintf(hdr, sizeof(hdr),
                      "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: %d\r\n\r\n",
                      len);
    sock_send(cfd, hdr, hn, 0);
    sock_send(cfd, html, len, 0);
}

static void send_redirect(int cfd, const char *location)
{
    char hdr[256];
    int hn = snprintf(hdr, sizeof(hdr),
                      "HTTP/1.1 302 Found\r\nLocation: %s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
                      location);
    sock_send(cfd, hdr, hn, 0);
}

static char *get_header_value(const char *request, const char *header_name)
{
    char *line_start = strstr(request, header_name);
    if (!line_start)
        return NULL;
    line_start += strlen(header_name);
    while (*line_start == ' ' || *line_start == ':')
        line_start++;
    char *line_end = strchr(line_start, '\r');
    if (!line_end)
        line_end = strchr(line_start, '\n');
    if (!line_end)
        return NULL;
    static char value[256];
    int len = line_end - line_start;
    if (len >= sizeof(value))
        len = sizeof(value) - 1;
    if (len <= 0)
        return NULL;
    strncpy(value, line_start, len);
    value[len] = '\0';
    while (len > 0 && (value[len - 1] == ' ' || value[len - 1] == '\t'))
        value[--len] = '\0';
    return value;
}

// =============== Captive Portal ===============
static int is_captive_portal_request(const char *uri, const char *host)
{
    if (!strcmp(uri, "/") ||
        !strncmp(uri, "/index.html", 11) ||
        !strncmp(uri, "/scan", 5) ||
        !strncmp(uri, "/status", 7) ||
        !strncmp(uri, "/wifi_cfg", 9) ||
        !strncmp(uri, "/favicon.ico", 12))
    {
        return 0;
    }
    if (!strcmp(uri, "/generate_204") ||
        !strcmp(uri, "/hotspot-detect.html") ||
        !strcmp(uri, "/ncsi.txt") ||
        !strcmp(uri, "/connectivity-check.html"))
    {
        return 1;
    }
    if (host && (strstr(host, "captive.apple.com") ||
                 strstr(host, "connectivitycheck.gstatic.com") ||
                 strstr(host, "msftconnecttest.com")))
    {
        return 1;
    }
    return 0;
}

// =============== 配网业务粘合点（不改变原逻辑） ===============
typedef struct
{
    char ssid[64];
    char pwd[64];
    char save;
} connect_args_t;

static void wifi_connect_task(void *priv)
{
    connect_args_t *a = (connect_args_t *)priv;
    os_time_dly(100);
    printf("wifi_connect_task: ssid='%s' save=%d\n", a->ssid, a->save);
    if (a->save == '1')
    {
        printf("wifi_store_mode_info save\n");
        wifi_store_mode_info(STA_MODE, a->ssid, a->pwd);
    }
    free(a);
}

static int start_wifi_connect_async(const char *ssid, const char *pwd, char save)
{
    connect_args_t *a = (connect_args_t *)malloc(sizeof(connect_args_t));
    if (!a)
        return -1;
    memset(a, 0, sizeof(*a));
    strncpy(a->ssid, ssid, sizeof(a->ssid) - 1);
    strncpy(a->pwd, pwd, sizeof(a->pwd) - 1);
    a->save = save;
    int pid = -1;
    if (thread_fork("wifi_connect_task", 23, 1024, 0, &pid, wifi_connect_task, a) != OS_NO_ERR)
    {
        free(a);
        return -2;
    }
    return 0;
}

// 将凭据抛给上层回调（如果设置）
static void invoke_on_credentials(const char *ssid, const char *pwd, int save)
{
    if (g_ctx.cbs.on_credentials)
    {
        g_ctx.cbs.on_credentials(ssid ? ssid : "", pwd ? pwd : "", save);
    }
}

// =============== HTTP请求处理 ===============
static void handle_client(void *cfd_hdl)
{
    char req[1024];
    int r = sock_recv(cfd_hdl, req, sizeof(req) - 1, 0);
    if (r <= 0)
        return;
    req[r] = 0;

    char *p = req;
    char *sp1 = strchr(p, ' ');
    if (!sp1)
        return;
    *sp1 = 0;
    char *method = p;
    char *sp2 = strchr(sp1 + 1, ' ');
    if (!sp2)
        return;
    *sp2 = 0;
    char *uri = sp1 + 1;
    char *host = get_header_value(req, "Host");

    if (strcmp(method, "GET"))
    {
        const char *resp = "HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\n\r\n";
        sock_send(cfd_hdl, resp, strlen(resp), 0);
        return;
    }

    if (g_ctx.cfg.enable_captive_portal && is_captive_portal_request(uri, host))
    {
        char location[64];
        snprintf(location, sizeof(location), "http://%d.%d.%d.%d/",
                 (g_ctx.cfg.captive_portal_ip >> 24) & 0xFF,
                 (g_ctx.cfg.captive_portal_ip >> 16) & 0xFF,
                 (g_ctx.cfg.captive_portal_ip >> 8) & 0xFF,
                 g_ctx.cfg.captive_portal_ip & 0xFF);
        send_redirect((int)cfd_hdl, location);
        return;
    }

    if (!strcmp(uri, "/") || !strncmp(uri, "/index.html", 11))
    {
        send_http_html((int)cfd_hdl, index_body, -1);
        return;
    }

    if (!strncmp(uri, "/scan", 5))
    {
        u32 num = 0;
        struct wifi_scan_ssid_info *arr = NULL;
        char buf[1024];
        int pos = 0;

        wifi_clear_scan_result();
        wifi_scan_req();
        os_time_dly(100);

        arr = wifi_get_scan_result(&num);
        pos += snprintf(buf + pos, sizeof(buf) - pos, "[");
        for (u32 i = 0; i < num && pos < (int)sizeof(buf) - 64; i++)
        {
            char esc[96];
            esc[0] = 0;
            if (arr[i].ssid)
                json_escape(esc, sizeof(esc), arr[i].ssid);
            pos += snprintf(buf + pos, sizeof(buf) - pos,
                            "%s{\"ssid\":\"%s\",\"rssi\":%d}",
                            (i ? "," : ""), esc, arr[i].rssi);
        }
        pos += snprintf(buf + pos, sizeof(buf) - pos, "]");
        if (arr)
            free(arr);
        wifi_clear_scan_result();

        send_http_text((int)cfd_hdl, "application/json", "%s", buf);
        return;
    }

    if (!strncmp(uri, "/status", 7))
    {
        enum wifi_sta_connect_state st = wifi_get_sta_connect_state();
        char st_str[24] = "UNKNOWN";
        int rssi = (int)wifi_get_rssi();
        switch (st)
        {
            case 0:
                strcpy(st_str, "IDLE");
                break;
            case 1:
                strcpy(st_str, "SCANNING");
                break;
            case 2:
                strcpy(st_str, "CONNECTING");
                break;
            case 3:
                strcpy(st_str, "CONNECTED");
                break;
            case 4:
                strcpy(st_str, "DISCONNECT");
                break;
            default:
                strcpy(st_str, "UNKNOWN");
                break;
        }
        send_http_text((int)cfd_hdl, "application/json",
                       "{\"state\":%d,\"state_str\":\"%s\",\"rssi\":%d,\"ip\":\"\"}", (int)st, st_str, rssi);
        return;
    }

    if (!strncmp(uri, "/wifi_cfg", 9))
    {
        const char *q = strchr(uri, '?');
        char ssid[64] = {0}, pwd[64] = {0}, save[8] = {0};
        if (q)
        {
            q++;
            query_get(q, "ssid", ssid, sizeof(ssid));
            query_get(q, "pwd", pwd, sizeof(pwd));
            query_get(q, "save", save, sizeof(save));
        }
        if (!ssid[0])
        {
            send_http_text((int)cfd_hdl, "application/json", "{\"ok\":0,\"msg\":\"SSID 不能为空\"}");
            return;
        }
        send_http_text((int)cfd_hdl, "application/json",
                       "{\"ok\":1,\"msg\":\"正在连接 WiFi...\",\"ssid\":\"%s\"}", ssid);

        // 维持你原有的异步连接逻辑
        start_wifi_connect_async(ssid, pwd, save[0] ? save[0] : '0');

        // 同时把凭据抛给上层回调（让上层决定是否接管连接/保存等）
        invoke_on_credentials(ssid, pwd, (save[0] == '1') ? 1 : 0);
        return;
    }

    // 其他请求重定向到 /
    {
        char location[64];
        snprintf(location, sizeof(location), "http://%d.%d.%d.%d/",
                 (g_ctx.cfg.captive_portal_ip >> 24) & 0xFF,
                 (g_ctx.cfg.captive_portal_ip >> 16) & 0xFF,
                 (g_ctx.cfg.captive_portal_ip >> 8) & 0xFF,
                 g_ctx.cfg.captive_portal_ip & 0xFF);
        send_redirect((int)cfd_hdl, location);
    }
}

// =============== 服务器任务 ===============
static void http_accept_loop(void *priv)
{
    void *srv = g_ctx.server_socket;
    void *cli = NULL;
    struct sockaddr_in raddr;
    socklen_t rlen = sizeof(raddr);

    while (1)
    {
        os_mutex_pend(&g_ctx.mtx, 0);
        int running = (g_ctx.state == WIFI_PROV_SERVER_RUNNING);
        os_mutex_post(&g_ctx.mtx);
        if (!running)
            break;

        cli = sock_accept(srv, (struct sockaddr *)&raddr, &rlen, NULL, NULL);
        if (!cli)
        {
            os_time_dly(10);
            continue;
        }
        handle_client(cli);
        sock_unreg(cli);
    }

    if (g_ctx.cbs.on_server_stopped)
        g_ctx.cbs.on_server_stopped();
}

static void http_server_task(void *priv)
{
    void *srv = NULL;
    struct sockaddr_in addr;
    u32 opt = 1;

    srv = sock_reg(AF_INET, SOCK_STREAM, 0, NULL, NULL);
    if (!srv)
    {
        os_mutex_pend(&g_ctx.mtx, 0);
        g_ctx.state = WIFI_PROV_SERVER_ERROR;
        os_mutex_post(&g_ctx.mtx);
        return;
    }
    g_ctx.server_socket = srv;

    if (sock_setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
    {
        sock_unreg(srv);
        g_ctx.server_socket = NULL;
        os_mutex_pend(&g_ctx.mtx, 0);
        g_ctx.state = WIFI_PROV_SERVER_ERROR;
        os_mutex_post(&g_ctx.mtx);
        return;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons(g_ctx.cfg.port);

    if (sock_bind(srv, (struct sockaddr *)&addr, sizeof(addr)))
    {
        sock_unreg(srv);
        g_ctx.server_socket = NULL;
        os_mutex_pend(&g_ctx.mtx, 0);
        g_ctx.state = WIFI_PROV_SERVER_ERROR;
        os_mutex_post(&g_ctx.mtx);
        return;
    }
    if (sock_listen(srv, 1) != 0)
    {
        sock_unreg(srv);
        g_ctx.server_socket = NULL;
        os_mutex_pend(&g_ctx.mtx, 0);
        g_ctx.state = WIFI_PROV_SERVER_ERROR;
        os_mutex_post(&g_ctx.mtx);
        return;
    }

    os_mutex_pend(&g_ctx.mtx, 0);
    g_ctx.state = WIFI_PROV_SERVER_RUNNING;
    os_mutex_post(&g_ctx.mtx);

    if (g_ctx.cbs.on_server_started)
        g_ctx.cbs.on_server_started();

    // accept loop
    thread_fork("prov_accept", 24, 2048, 0, &g_ctx.accept_pid, http_accept_loop, NULL);

    // 等待停止信号
    while (1)
    {
        os_mutex_pend(&g_ctx.mtx, 0);
        int stopping = (g_ctx.state == WIFI_PROV_SERVER_STOPPING);
        int running = (g_ctx.state == WIFI_PROV_SERVER_RUNNING);
        os_mutex_post(&g_ctx.mtx);
        if (!running && !stopping)
            break;
        os_time_dly(100);
    }

    // 关闭server
    if (g_ctx.server_socket)
    {
        sock_unreg(g_ctx.server_socket);
        g_ctx.server_socket = NULL;
    }

    os_mutex_pend(&g_ctx.mtx, 0);
    g_ctx.state = WIFI_PROV_SERVER_STOPPED;
    os_mutex_post(&g_ctx.mtx);
}

// =============== 异步启动/停止封装 ===============
static void prov_start_trampoline(void *arg)
{
    // 防止重复启动
    os_mutex_pend(&g_ctx.mtx, 0);
    if (g_ctx.state != WIFI_PROV_SERVER_STARTING)
    {
        os_mutex_post(&g_ctx.mtx);
        return;
    }
    os_mutex_post(&g_ctx.mtx);

    int pid = -1;
    if (thread_fork("prov_http", 24, 2048, 0, &pid, http_server_task, NULL) != OS_NO_ERR)
    {
        os_mutex_pend(&g_ctx.mtx, 0);
        g_ctx.state = WIFI_PROV_SERVER_ERROR;
        os_mutex_post(&g_ctx.mtx);
        return;
    }
}

static void prov_stop_trampoline(void *arg)
{
    os_mutex_pend(&g_ctx.mtx, 0);
    if (g_ctx.state == WIFI_PROV_SERVER_RUNNING)
    {
        g_ctx.state = WIFI_PROV_SERVER_STOPPING;
    }
    os_mutex_post(&g_ctx.mtx);
}

// =============== 自启动线程（等待AP就绪） ===============
int __attribute__((weak)) wifi_prov_wait_ap_ready(void)
{
    int tries = 6; // 最多等6秒
    while (tries--)
    {
        if (wifi_is_on())
        {
            os_time_dly(100);
            return 0;
        }
        os_time_dly(100);
    }
    return -1;
}

static void prov_autostart_task(void *arg)
{
    if (!g_ctx.autostart_enabled)
        return;
    if (wifi_prov_wait_ap_ready() == 0)
    {
        wifi_provisioning_server_start_async();
    }
}

// =============== 公共API ===============
int wifi_provisioning_server_init(const wifi_prov_server_config_t *cfg)
{
    if (g_ctx.initialized)
        return 0;

    memset(&g_ctx, 0, sizeof(g_ctx));
    os_mutex_create(&g_ctx.mtx);

    g_ctx.cfg.port = DEFAULT_HTTP_PORT;
    g_ctx.cfg.captive_portal_ip = DEFAULT_CAPTIVE_IP;
    g_ctx.cfg.auto_start = 1;
    g_ctx.cfg.enable_captive_portal = 1;

    if (cfg)
        g_ctx.cfg = *cfg;

    g_ctx.autostart_enabled = g_ctx.cfg.auto_start ? 1 : 0;
    g_ctx.state = WIFI_PROV_SERVER_STOPPED;
    g_ctx.initialized = 1;

    if (g_ctx.autostart_enabled)
    {
        int pid;
        thread_fork("prov_autostart", 23, 512, 0, &pid, prov_autostart_task, NULL);
    }
    return 0;
}

int wifi_provisioning_server_set_callbacks(const wifi_prov_server_callbacks_t *cbs)
{
    if (!g_ctx.initialized)
        return -1;
    if (cbs)
        g_ctx.cbs = *cbs;
    else
        memset(&g_ctx.cbs, 0, sizeof(g_ctx.cbs));
    return 0;
}

int wifi_provisioning_server_enable_autostart(int enable)
{
    g_ctx.autostart_enabled = enable ? 1 : 0;
    return 0;
}

int wifi_provisioning_server_start_async(void)
{
    if (!g_ctx.initialized)
        return -1;

    // 等待AP就绪（异步接口也保护一下）
    if (wifi_prov_wait_ap_ready() != 0)
    {
        // AP 未就绪，略等待再尝试
    }

    os_mutex_pend(&g_ctx.mtx, 0);
    if (g_ctx.state != WIFI_PROV_SERVER_STOPPED)
    {
        os_mutex_post(&g_ctx.mtx);
        return 0;
    }
    g_ctx.state = WIFI_PROV_SERVER_STARTING;
    os_mutex_post(&g_ctx.mtx);

    int pid;
    if (thread_fork("prov_start", 23, 768, 0, &pid, prov_start_trampoline, NULL) != OS_NO_ERR)
    {
        os_mutex_pend(&g_ctx.mtx, 0);
        g_ctx.state = WIFI_PROV_SERVER_ERROR;
        os_mutex_post(&g_ctx.mtx);
        return -2;
    }
    return 0;
}

int wifi_provisioning_server_stop_async(void)
{
    if (!g_ctx.initialized)
        return -1;

    os_mutex_pend(&g_ctx.mtx, 0);
    if (g_ctx.state != WIFI_PROV_SERVER_RUNNING)
    {
        os_mutex_post(&g_ctx.mtx);
        return 0;
    }
    os_mutex_post(&g_ctx.mtx);

    int pid;
    if (thread_fork("prov_stop", 23, 512, 0, &pid, prov_stop_trampoline, NULL) != OS_NO_ERR)
    {
        return -2;
    }
    return 0;
}

wifi_prov_server_state_t wifi_provisioning_server_get_state(void)
{
    wifi_prov_server_state_t st;
    os_mutex_pend(&g_ctx.mtx, 0);
    st = g_ctx.state;
    os_mutex_post(&g_ctx.mtx);
    return st;
}

int wifi_provisioning_server_notify_finished(wifi_prov_result_t result)
{
    if (g_ctx.cbs.on_finished)
        g_ctx.cbs.on_finished(result);
    return wifi_provisioning_server_stop_async();
}

void wifi_provisioning_server_deinit(void)
{
    if (!g_ctx.initialized)
        return;
    wifi_provisioning_server_stop_async();

    // 简单等待退出
    int tries = 5;
    while (tries--)
    {
        if (wifi_provisioning_server_get_state() == WIFI_PROV_SERVER_STOPPED)
            break;
        os_time_dly(100);
    }

    os_mutex_del(&g_ctx.mtx, OS_DEL_ALWAYS);
    memset(&g_ctx, 0, sizeof(g_ctx));
}

// ------ 自启动 ------
static void http_server_start(void *priv)
{
    printf("http_server_start\n");

    // wifi_prov_server_config_t
    wifi_provisioning_server_init(NULL);
}

// 应用入口：建议在 AP 模式下运行，STA 也可（有 IP 后即可）
static void c_main(void *priv)
{
    if (thread_fork("http_server_start", 23, 512, 0, NULL, http_server_start, NULL) != OS_NO_ERR)
    {
        printf("thread fork fail\n");
    }
}
late_initcall(c_main);